﻿#pragma once

#include <glad/glad.h>
//
#include <GLFW/glfw3.h>
#include <learn/ais/ais_camera.h>

// 由于 STB_IMAGE_IMPLEMENTATION 将 std_image 被编译为 .cpp
// 文件，因此不放在头文件中
#define STB_IMAGE_IMPLEMENTATION
#include <third/stb_image.h>

#include <fstream>
#include <iostream>
#include <sstream>
#include <string>

using namespace xi;
using namespace xi::geom;
using namespace xi::ais;

namespace
{

enum class Action3d
{
    None,
    Rotate,
    Scale
};

float angleRate = 0.25f;
float scaleRate = 2.0f;
Action3d mouseAction = Action3d::None;

std::string readFileToString(const std::string &filePath)
{
    std::ifstream file(filePath); // 打开文件
    if (!file.is_open())
        throw std::runtime_error("Failed to open file: " + filePath);

    std::stringstream buffer; // 创建一个字符串流
    buffer << file.rdbuf();   // 将文件内容读取到字符串流中
    return buffer.str();      // 返回字符串
}

void loadTexture(const char *file)
{
    // 加载图片前翻转图片
    stbi_set_flip_vertically_on_load(true);

    // glTexImage2D 生成纹理，然后 glGenerateMipmap 产生多级渐远纹理
    int width, height, nrComponents;
    unsigned char *data = stbi_load(file, &width, &height, &nrComponents, 0);
    if (data)
    {
        // 判定图像数据类型
        GLenum format = GL_RGB;
        if (nrComponents == 1)
            format = GL_RED;
        else if (nrComponents == 3)
            format = GL_RGB;
        else if (nrComponents == 4)
            format = GL_RGBA;

        glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);

        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

        stbi_image_free(data);
    }
    else
    {
        std::cerr << "Texture failed to load at path: " << file;
        stbi_image_free(data);
    }
}

void loadCubeTexture(const std::vector<const char *> &files)
{
    // 取消纹理翻转
    stbi_set_flip_vertically_on_load(false);

    // glTexImage2D 生成纹理，然后 glGenerateMipmap 产生多级渐远纹理
    int width, height, nrComponents;
    for (unsigned int i = 0; i < files.size(); i++)
    {
        unsigned char *data = stbi_load(files[i], &width, &height, &nrComponents, 0);

        if (data)
        {
            // 判定图像数据类型
            GLenum format = GL_RGB;
            if (nrComponents == 1)
                format = GL_RED;
            else if (nrComponents == 3)
                format = GL_RGB;
            else if (nrComponents == 4)
                format = GL_RGBA;

            glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE,
                         data);
            glGenerateMipmap(GL_TEXTURE_CUBE_MAP);

            stbi_image_free(data);
        }
        else
        {
            std::cerr << "Texture failed to load at path: " << files[i];
            stbi_image_free(data);
        }
    }

    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
}

} // namespace

class Shader
{
  public:
    Shader(const char *vertexPath, const char *fragmentPath, const char *geometryPath)
    {
        unsigned int vertex = _load(vertexPath, GL_VERTEX_SHADER);
        unsigned int fragment = _load(fragmentPath, GL_FRAGMENT_SHADER);
        unsigned int geometry = 0;

        // shader Program
        m_ID = glCreateProgram();
        glAttachShader(m_ID, vertex);
        glAttachShader(m_ID, fragment);

        // 如果载入几何着色器
        if (geometryPath != nullptr)
        {
            geometry = _load(geometryPath, GL_GEOMETRY_SHADER);
            glAttachShader(m_ID, geometry);
        }

        glLinkProgram(m_ID);
        _check_compile_errors(m_ID, "PROGRAM");

        // 删除着色器
        glDeleteShader(vertex);
        glDeleteShader(fragment);

        if (geometryPath != nullptr)
            glDeleteShader(geometry);
    }

    template <typename T> void set_value(std::string variable, const T &val)
    {
        char *var = const_cast<char *>(variable.c_str());
        if constexpr (std::is_same_v<T, int>)
            glUniform1i(glGetUniformLocation(m_ID, var), val);
        else if constexpr (std::is_same_v<T, float>)
            glUniform1f(glGetUniformLocation(m_ID, var), val);
        else if constexpr (std::is_same_v<T, Vec2f>)
            glUniform2f(glGetUniformLocation(m_ID, var), val.x(), val.y());
        else if constexpr (std::is_same_v<T, Vec3f>)
            glUniform3f(glGetUniformLocation(m_ID, var), val.x(), val.y(), val.z());
        else if constexpr (std::is_same_v<T, Vec4f>)
            glUniform4f(glGetUniformLocation(m_ID, var), val.x(), val.y(), val.z(), val.w());
        else if constexpr (std::is_same_v<T, Mat4x4<float>>)
        {
            unsigned int loc = glGetUniformLocation(m_ID, var);

            // GL_TRUE 表示矩阵转置以后传入
            glUniformMatrix4fv(loc, 1, GL_TRUE, val.data);
        }
        else
            throw std::runtime_error("Unsupported type!");
    }

    void enable()
    {
        glUseProgram(m_ID);
    }

    ~Shader()
    {
        glDeleteProgram(m_ID);
    }

  private:
    unsigned int _load(const char *path, int type)
    {
        // 得到字符串代码
        std::string code = readFileToString(path);
        const char *shaderCode = code.c_str();

        // 编译着色器
        unsigned int shader = glCreateShader(type);
        glShaderSource(shader, 1, &shaderCode, NULL);
        glCompileShader(shader);

        // 错误检查
        switch (type)
        {
        case GL_VERTEX_SHADER:
            _check_compile_errors(shader, "VERTEX");
            break;
        case GL_FRAGMENT_SHADER:
            _check_compile_errors(shader, "FRAGMENT");
            break;
        case GL_GEOMETRY_SHADER:
            _check_compile_errors(shader, "GEOMETRY");
            break;
        }

        return shader;
    }

    void _check_compile_errors(unsigned int shader, std::string type)
    {
        int success;
        char infoLog[1024];
        if (type != "PROGRAM")
        {
            glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
            if (!success)
            {
                glGetShaderInfoLog(shader, 1024, NULL, infoLog);
                std::cerr << "ERROR::SHADER_COMPILATION_ERROR of type: " << type << "\n"
                          << infoLog << "\n -- --------------------------------------------------- -- " << std::endl;
            }
        }
        else
        {
            glGetProgramiv(shader, GL_LINK_STATUS, &success);
            if (!success)
            {
                glGetProgramInfoLog(shader, 1024, NULL, infoLog);
                std::cerr << "ERROR::PROGRAM_LINKING_ERROR of type: " << type << "\n"
                          << infoLog << "\n -- --------------------------------------------------- -- " << std::endl;
            }
        }
    }

  protected:
    unsigned int m_ID;
};

std::shared_ptr<Camera> camera;

void GLFWWindowResize(GLFWwindow *window, int width, int height)
{
    glViewport(0, 0, width, height);
    camera->resize(width, height);
}

void GLFWMouseButtonCallback(GLFWwindow *window, int button, int action, int mode)
{
    if (button == GLFW_MOUSE_BUTTON_MIDDLE)
    {
        if (mouseAction == Action3d::None && action == GLFW_PRESS)
            mouseAction = Action3d::Rotate;

        if (mouseAction == Action3d::Rotate && action == GLFW_RELEASE)
            mouseAction = Action3d::None;
    }
}

void GLFWCursorPosCallback(GLFWwindow *window, double xpos, double ypos)
{
    static float px, py;

    if (mouseAction == Action3d::Rotate)
    {
        camera->rotate((px - xpos) * angleRate, (py - ypos) * angleRate);
    }

    px = xpos;
    py = ypos;
}

void GLFWScrollCallback(GLFWwindow *window, double xoffset, double yoffset)
{
    if (mouseAction == Action3d::None)
    {
        mouseAction = Action3d::Scale;

        double px, py;
        glfwGetCursorPos(window, &px, &py);

        camera->translate(Vec2f{(float)px, (float)py}, scaleRate * -yoffset);

        mouseAction = Action3d::None;
    }
}

void GLFWKeyCallback(GLFWwindow *window, int key, int scancode, int action, int mods)
{
    if (action == GLFW_PRESS)
    {
        if (key == GLFW_KEY_ESCAPE)
            glfwSetWindowShouldClose(window, GLFW_TRUE);
        if (key == GLFW_KEY_UP)
            camera->rotate_front(Vec3f{0.0f, 0.0f, 1.0f} * angleRate);
        if (key == GLFW_KEY_DOWN)
            camera->rotate_front(Vec3f{0.0f, 0.0f, -1.0f} * angleRate);
        if (key == GLFW_KEY_0)
            camera->set_center(Vec3f{0.0f, 0.0f, 0.0f});
        if (key == GLFW_KEY_1)
            camera->upright_view();
        if (key == GLFW_KEY_3)
            camera->right_view();
        if (key == GLFW_KEY_7)
            camera->top_view();
        if (key == GLFW_KEY_9)
            camera->bottom_view();
    }
}

GLFWwindow *initProgram()
{
    // 初始化
    glfwInit();

    // 配置版本信息，启用核心模式
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    // 创建窗口
    GLFWwindow *window = glfwCreateWindow(800, 600, "LearnOpenGL", 0, 0);
    if (window == nullptr)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return nullptr;
    }
    glfwMakeContextCurrent(window);

    // 初始化 GLAD 来管理 OpenGL 函数指针
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return nullptr;
    }

    // 设置视口
    glViewport(0, 0, 800, 600);
    camera = std::make_shared<Camera>(800, 600);

    // 注册函数
    glfwSetFramebufferSizeCallback(window, GLFWWindowResize);
    glfwSetScrollCallback(window, GLFWScrollCallback);
    glfwSetMouseButtonCallback(window, GLFWMouseButtonCallback);
    glfwSetCursorPosCallback(window, GLFWCursorPosCallback);
    glfwSetKeyCallback(window, GLFWKeyCallback);

    return window;
}